红黑树
我们知道对于二叉搜索树而言,无法保证树的平衡性,从而使得进行操作的时候时间复杂度在O(logn)与O(n)之间。这样是不稳定的。而2-3树则借助于3-结点和临时的4-结点,通过分解,解决了平衡问题。例如对于插入,在最终是3-结点的情况下,临时构成4-结点,然后再分解成3个2-结点,这样令树高度+1,但是整体平衡性不变。而红黑树就是为了实现自平衡这个功能而对2-3树进行了扩展。而红黑树仍然属于一棵二叉查找树,只是多了红结点与黑结点,来模仿2-结点和3-结点。所以对于二叉搜索树的方法,很多都能在红黑树上使用。而红黑树的关于自平衡的很多理论,来源于2-3树。下面是一棵简单的红黑树:
其实按照标准定义,叶子节点仍然有左右两个引用,也就是说,任何一个结点都一定有2个子树,就算这个子树是空的。而这些结点分为红色与黑色。注意看上面的箭头,也有红黑之分。或者说,我们虽然在定义Node结点的时候,认为某个节点的颜色有红黑之分,但是实际上,这个红黑是指链接是红色还是黑色。
也就是说,如果某结点的颜色是红色,是说连接到该结点的链接是红色,而根的颜色永远为黑色。
这里提一嘴,红黑树非常依赖图像演示,推荐找视频或者找各种动图来学习红黑树,能够非常快理解旋转之类的操作。
红黑树与2-3树
为什么一定要对链接区分红黑?如果就上面的图进行变换,我们不管黑色,将红色的连接平着画,可以得到这个图:
可以看到,这样来表示,树的高度是一致的,如果我们仔细看,是不是可以认为,红色链接连接的2个结点可以组合起来作为一个2-3树中的3-结点。这样,就把一棵红黑树转换成了2-3树。而我们认为2-3树是完美平衡的,那么是否说明了,红黑树也是完美平衡。
红链接将2个2-结点连接起来构成一个3-结点,黑链接则是2-3树中的普通链接。那么对于红黑树而言,仍然可以使用二叉查找树中的搜索之类的,与颜色无关的方法。而例如插入和删除,我们可以认为在处理过程中,使用2-结点、3-结点、4-结点之间的组合与删除,来完成自平衡。
《算法》书中认为:这样用红黑链接表示的2-3树,是红黑二叉查找树。
红黑树的性质
对红黑树有这样的定义:
1.红链接均为左链接。
2.两个红链接不能相连在一起。
3.红黑树是完美黑色平衡,即任意空结点到根节点所经过的黑链接完全一致。
4.空结点的链接颜色是黑色。
第一条有点疑问,因为有的资料说可以将红链接作为右链接,这个文章统一认为是只能为左。因为上面的性质,我们需要额外确定一些性质:
1.根节点的链接颜色必定为黑。这里其实无所谓,因为根没有父链接,但是结点中有color,所以就默认为黑了。
2.我们认为红黑树“有数据”的结点都不是叶子结点。叶子结点只能为null或者NIL。
既然我们认为红黑树就是2-3树的一种,那么就可以理解为什么不能两个红链接连在一起了,这就像一个临时的4-结点,我们允许其存在,但是为了保证平衡,需要分解。
红黑树的颜色
其实上面写了一点,虽然我们将颜色定义到结点中,但是只是因为我们无法对链接声明数据结构罢了,应该认为是指向该结点的链接的颜色。
在实际的代码中,可以用常量来表示红黑,调用一个方法可以判定是黑是红即可。例如:
private boolean isRed(Node node){
if(node == null) return false;
return node.color == RED;
}
写一个就行了,没必要写个isBlack。具体的实现可以看下面对红黑树的定义以及结点的定义。这里并不是写进Node类,而是RBT类中的方法,因为这样会使得后面的算法更加灵活。
红黑树的声明以及Node结点的实现
我们可以像容器一样,声明一个“红黑树”,而将Node结点作为其内部类。这样可以保证比较广泛地应用。当然也需要使用泛型,因为不可能固定数据类型。红黑树在Java中是TreeMap和TreeSet的底层,对于Map这种数据结构,我们需要使用Key-Value的结构,那么在结点中,声明两种元素就可以了,为了保证顺序,可以令Key实现comparator接口,从而保证按照Key顺序排序,同时完成Map映射关系。这样,我们可以得到这样的数据结构:
class RBT<Key extends Comparable<Key>, Value>{
private static final boolean BLACK = false;
private static final boolean RED = true;
private Node root;
private class Node{
private Key key;
private Value value;
private Node left;
private Node right;
private boolean color;
public Node(Key key,Value value){
this.key = key;
this.value = value;
this.color = RED;
}
}
}
在Node结点中,我是定义了boolean作为红黑判定,这个可以任意改变,例如int,都可以。而这两个常量是在RBT类里面而不是Node,因为我们需要在其他算法里也使用这些常量。
红黑树自平衡的操作-旋转
会AVL树的话,可能就非常简单能够理解旋转操作。旋转的作用是在进行例如插入、删除操作的时候,根据不同的情况(主要是因为平衡被破坏了),从而进行不同的旋转操作使得以及破坏平衡的树重新回归平衡。这也是我们说“自平衡”的原因。
红黑树的话,旋转其实有点类似2-3树中的结点分解与临时合并。这个等后面再分析。旋转其实就是左旋和右旋。接收一个结点,转就完事了。而不同情况下如何调用旋转来保持平衡,这是插入、删除等方